﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Xml.Serialization;
using System.IO;
using System.Diagnostics;
using System.ComponentModel;
using System.Runtime.Serialization;
using System.Xml;
using MoneyWentWhere.DataModel.Binding;
using MoneyWentWhere.DataModel.Storage;

namespace MoneyWentWhere.DataModel
{
    [DataContract(Name = "MoneyDataSet")]
    public class MoneyDataSet : INotifyPropertyChanged, IExtensibleDataObject, IDisposable
    {
        public String Filename { get; set;  }
        public DataFormatX DataFormat { get; set; }
        public String Password { get; set; }

        // Direct people viewing the XML in the editor to the program site 
        [DataMember]
        private String ProgramWebsite = "http://code.google.com/p/moneywentwhere/";

        [DataMember(Name = "Accounts")]
        public AccountList Accounts;

        [DataMember(Name = "DefaultCurrency")]
        public Currency DefaultCurrency = new Currency("USD");

        // Currency list. Perhaps better to keep it with global data
        [DataMember(Name = "Currencies")]
        public readonly CurrencyList Currencies = new CurrencyList();

        // Categories 
        [DataMember(Name = "Categories")]
        public readonly CategorySet Categories = new CategorySet();

        // All objects with Guids
        public Dictionary<Guid, object> _objectByGuid = new Dictionary<Guid,object>();

        internal MoneyDataSet()
        {
            Accounts = new AccountList(this);
        }

        public MoneyDataSet(string filename, DataFormatX dataFormat) 
            : this()
        {
            Filename = filename;
            DataFormat = dataFormat;
        }

        public Account CreateAccount(String name)
        {
            return CreateAccount(name, 0, -1);
        }

        public Account CreateAccount(String name, decimal openingBalance)
        {
            return CreateAccount(name, openingBalance, -1);
        }

        /// <summary>
        /// Create a new account. Adds an opening balance transaction to it.
        /// </summary>
        /// <param name="name"></param>
        /// <param name="index"></param>
        /// <returns></returns>
        public Account CreateAccount(String name, decimal openingBalance, int index)
        {
            Trace.Write("Creating account @" + index);

            Account a = new Account(this, Guid.Empty, name);

            if (index == -1) { Accounts.Add(a); }
            else { Accounts.Insert(index, a); }

            Transaction t = a.CreateTransaction(TransactionKind.Reconcile, openingBalance);
            t.Comment = "Opening Balance";

            return a;
        }

        /// <summary>
        /// Get a uniquely identified object by its guid
        /// </summary>
        /// <typeparam name="T"></typeparam>
        /// <param name="guid"></param>
        /// <returns></returns>
        public T GetObject<T>(Guid guid) where T : class
        {
            object o;
            if (_objectByGuid.TryGetValue(guid, out o))
            {
                return o as T;                
            }
            return null;
        }

        internal void StoreObject(Guid guid, object o)
        {
            if (guid == Guid.Empty) { throw new ArgumentException("Empty guid"); }
            _objectByGuid.Add(guid, o);
        }

        /// <summary>
        /// Get the exchange rate before the given date, -1 if unavailable
        /// xr = toAmount / fromAmount 
        /// </summary>
        /// <param name="from"></param>
        /// <param name="to"></param>
        /// <param name="beforeDate"></param>
        /// <returns></returns>
        public decimal GetExchangeRateBefore(Currency from, Currency to, DateTime beforeDate)
        {
            if (from == to) { return 1; }

            Transaction xrTransfer = GetExchangeRateTransactions(from, to, d => d < beforeDate)
                .OrderBy(tx => tx.Date)
                .LastOrDefault();

            if (xrTransfer == null) { return -1; }
            return Currency.ExchangeRate(xrTransfer.Amount, xrTransfer.TransferToTx.Amount);
        }

        public decimal GetExchangeRateNear(Currency from, Currency to, DateTime targetDate)
        {
            if (from == to) { return 1; }

            // Before or on targetDate
            Transaction xrTransfer = GetExchangeRateTransactions(from, to, d => d <= targetDate)
                .OrderBy(tx => tx.Date)
                .LastOrDefault();

            if (xrTransfer == null)
            {
                // After target date
                xrTransfer = GetExchangeRateTransactions(from, to, d => d > targetDate)
                    .OrderBy(tx => tx.Date)
                    .FirstOrDefault();
            }

            // Nothing found
            if (xrTransfer == null) { return -1; }
            return Currency.ExchangeRate(xrTransfer.Amount, xrTransfer.TransferToTx.Amount);
        }

        IEnumerable<Transaction> GetExchangeRateTransactions(Currency from, Currency to,
            Predicate<DateTime> dateFilter)
        {
            return Accounts.SelectMany(ac => ac.Transactions
                .Where(tx =>
                    tx.Kind == TransactionKind.Transfer &&
                    tx.Account.Currency == from &&
                    tx.TransferToTx.Account.Currency == to &&
                    dateFilter(tx.Date)));
        }


        #region Load/Save/Create

        public void Save()
        {
            Save(Filename, DataFormat);
        }

        public void Save(String newFilename)
        {
            if (newFilename != Filename)
            {
                DataFormat = DataFormatX.GetFormat(newFilename);
            } 
            Save(newFilename, DataFormat);
        }

        public void Save(String newFilename, DataFormatX newFormat)
        {
            if (newFormat == null) { throw new ArgumentNullException("newFormat"); }

            // Use the password
            IHasPassword pf = newFormat as IHasPassword;
            if (pf != null) { pf.Password = Password; }
            
            newFormat.Save(this, newFilename);
        }

        public static MoneyDataSet Load(String filename)
        {
            return Load(filename, null);
        }

        public static MoneyDataSet Load(String filename, String password)
        {
            DataFormatX format = DataFormatX.GetFormat(filename);

            // Use the password
            IHasPassword pf = format as IHasPassword;
            if (pf != null) { pf.Password = password; }
 
            return format.Load(filename);
        }

        #endregion

        #region Common Serialization / Notification methods
        public event PropertyChangedEventHandler PropertyChanged;

        void OnPropertyChanged(String name)
        {
            if (PropertyChanged != null)
            {
                PropertyChanged(this, new PropertyChangedEventArgs(name));
            }
        }

        [Browsable(false)]
        public ExtensionDataObject ExtensionData { get; set; }

        [Browsable(false)]
        public object ExtendedFileData { get; set; }

        #endregion

        #region IDisposable Members

        public void Dispose()
        {
            if (ExtendedFileData is IDisposable)
            {
                ((IDisposable)ExtendedFileData).Dispose();
                ExtendedFileData = null;
            }
        }

        #endregion
    }
}
